package org.mentalsmash.crossroads;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;
import org.mentalsmash.crossroads.parsing.CrosswordParser;
import org.mentalsmash.crossroads.parsing.ParsingException;

/**
 * This class is meant to be used as an easy-to-use interface for main crossroads facilities
 * @author Andrea Reale
 */
public class CrossroadsManager {

	private Logger log = Logger.getLogger(CrossroadsManager.class);
	private CrosswordParser _parser;
	private Configuration _conf;
	private final Pattern _extensionPattern;
	
	private DictionarySource _ds;

	
	/**
	 * Creates a crossword manager initialized with the configuration read from conf/crossroads.conf
	 * @throws ParsingException if the parser specified in the configuration file could not be initialized
	 */
	public CrossroadsManager() throws ParsingException{
		_conf = Configuration.getInstance();
		_extensionPattern = Pattern.compile("(.+)\\." + getConfiguration().getSchemaExtension() + "$");
		_parser = newParser();
		setDictionarySource();
	}

	/**
	 * Set the parser that will be used by the manager to parse files
	 * @param parser the new parser to use
	 */
	public void setParser(CrosswordParser parser) {
		_parser = parser;
	}

	/**
	 * @return a reference to the parser used by the manager
	 */
	public CrosswordParser getParser() {
		return _parser;
	}
	
	/**
	 * @return The instance of configuration used by the manager
	 */
	public Configuration getConfiguration() {
		return _conf;
	}
	
	

	/**
	 * Set the dictionary source used by the manager
	 * @param ds a dictionary manager that should be used
	 */
	public void setDictionarySource(DictionarySource ds) {
		_ds = ds;
	}

	/**
	 * @return the dictionary source that is being used by the manager
	 */
	public DictionarySource getDictionarySource() {
		return _ds;
	}
	

	
	/**
	 * Parse sources to obtain a Crossword instance. This method searches for both schema definition file and definitions file.
	 * Definitions file name MUST have the same name of the schema file name (except for the extension of course), and must be in the same
	 * directory
	 * 
	 * @param crosswordSchemaSource the path to the schema definitions source file. Note that it must have the correct extension according to crossroads configuration file.
	 * @return a Crossword instance as described in the sources
	 * @throws ParsingException if the source contains syntactic or semantic errors
	 * @throws FileNotFoundException if one of schema source or definitions source is not found.
	 */
	public Crossword parseBoth(String crosswordSchemaSource) throws ParsingException, FileNotFoundException{
		
		File schemaFile = checkSchemaSource(crosswordSchemaSource);
		
	    File defsFile = retrieveDefinitionsSource(schemaFile);
		
		Crossword parsed = getParser().parseCrosswordSource(schemaFile, defsFile);
		
		return parsed;
	}

	/**
	 * Parse sources to obtain a Crossword instance. This method usesonly the schema definitions file and does not search for the corrisponding
	 * definitions source. Definitions in the created crossword instance will be set to the empty string.
	 * 
	 * @param crosswordSchemaSource the path to the schema definitions source file. Note that it must have the correct extension according to crossroads configuration file.
	 * @return a Crossword instance as described in the sources
	 * @throws ParsingException if the source contains syntactic or semantic errors
	 * @throws FileNotFoundException if one of schema source or definitions source is not found.
	 */
	public Crossword parseSchemaOnly(String crosswordSchemaSource) throws ParsingException, FileNotFoundException {
		File schemaFile = checkSchemaSource(crosswordSchemaSource);
		FileInputStream fis = new FileInputStream(schemaFile);
		Crossword cwd = null;
		try {
			cwd = getParser().parseCrosswordSchemaOnly(new FileInputStream(schemaFile));
		}finally{
			try {
				fis.close();
			} catch (IOException e) {
				log.warn("Something bad happened while closing input source stream: " + e.getMessage());
				e.printStackTrace();
			}
		}
		
		return cwd;
	}
	
	
	/**
	 * Prints the prolog theory describing the solution for the input crossword to the given output stream
	 * @param input a Crossword instance
	 * @param where an OutputStream where to write the prolog theory
	 */
	public void printPrologTheory(Crossword input, OutputStream where){
		PrologExporter pe = new PrologExporter(getDictionarySource());
		pe.buildSolverTheory(input, where);
	}
	
	/**
	 * Prints an xhtml page representing the crossword given as input to the given outupt stream
	 * @param input the crossword to externalize in xhtml 
	 * @param where an OutputStream where to write the xml document
	 */
	public void generateXhtmlDescription(Crossword input,boolean withDefinitions, OutputStream where){
		XhtmlExporter xe = new XhtmlExporter(withDefinitions);
		xe.buildXhtmlDocument(input, where);
	}
	
	/**
	 * Prints an svg document that represents a crossword given as inpout
	 * @param input the crossword to picture
	 * @param withDefinitions true if you want definitions embedded in the svg
	 * @param where an output stream where to write the svg document
	 */
	public void generateSvgDescription(Crossword input, boolean withDefinitions, OutputStream where){
		SVGExporter se = new SVGExporter(withDefinitions,true);
		se.buildSvgDocument(input, where);
	}
	
	/**
	 * Tries to solve the given crossword instance using the Solving Engine set in the config file (or dinamically set using
	 * setSolvingEngine()
	 * @param toSolve the crossword instance to solve
	 * @throws SolvingException if no solution could be found
	 */
	public void solve(Crossword toSolve) throws SolvingException{
		SolvingEngine se = newSolvingEngine();
		se.solve(toSolve);
	}
	
	
	private File retrieveDefinitionsSource(File schemaFile)
			throws FileNotFoundException {
		
		Matcher mtch = _extensionPattern.matcher(schemaFile.getPath());
		if(!mtch.matches()){
			log.fatal("Passed schema file name does not have a coherent structure" );
		    System.err.println("FATAL: see logs for details.");
			System.exit(-5);
		}
		
		String definitionsFileName = mtch.group(1) +"." + getConfiguration().getDefintionsExtension();
		
		File defsFile = new File(definitionsFileName);
		if(!defsFile.exists() || !defsFile.canRead())
			throw new FileNotFoundException("" + defsFile.toString() + " does not exist or it cannot be read.");
		return defsFile;
	}
	
	private File checkSchemaSource(String pathToSource) throws ParsingException, FileNotFoundException {

		//if the passed arg has not the right extension
		if(!_extensionPattern.matcher(pathToSource).matches()){
			throw new ParsingException("Input file has the wrong extension. It should be " + getConfiguration().getSchemaExtension() );
		}
		File schemaFile = new File(pathToSource);
		
		if(!schemaFile.exists() || !schemaFile.canRead())
			throw new FileNotFoundException("" + schemaFile.toString() + " does not exist or it cannot be read.");
		
		return schemaFile;
	}

	
	// sets the dictionary source dynamically instantiating the class read from the configuration file
	private void setDictionarySource(){
		Class<?> dicSourceClass = null;
		try {
			dicSourceClass = Class.forName(getConfiguration().getDictionarySource());
		} catch (ClassNotFoundException e) {
			log.fatal("Invalid Dictionary Source class: " + getConfiguration().getDictionarySource());
			e.printStackTrace();
			System.exit(-2);
		}
		Object ds = null;
		try {
			ds = dicSourceClass.newInstance();
		} catch (InstantiationException e) {
			log.fatal("Failed to instantiate class " + getConfiguration().getDictionarySource());
			e.printStackTrace();
			System.exit(-2);
		} catch (IllegalAccessException e) {
			log.fatal("Failed to instantiate class " + getConfiguration().getDictionarySource());
			e.printStackTrace();
			System.exit(-2);
		}
		if(ds instanceof DictionarySource)
			setDictionarySource((DictionarySource)ds);
		else {
			log.fatal("Class " + getConfiguration().getDictionarySource() + " does not implement DictionarySource interface");
			System.err.println("Unable to instantiate dictionary source. See logs.");
			System.exit(-2);
		}
	}
	
	// sets the parser dynamically instantiating the class read from the configuration file
	private CrosswordParser newParser(){
		Class<?> parserClass = null;
		CrosswordParser prs = null;
		try {
			parserClass = Class.forName(getConfiguration().getParserEngine());
		} catch (ClassNotFoundException e) {
			log.fatal("Invalid CrosswordParser class: " + getConfiguration().getParserEngine());
			e.printStackTrace();
			System.exit(-2);
		}
		Object parser = null;
		try {
			parser = parserClass.newInstance();
		} catch (InstantiationException e) {
			log.fatal("Failed to instantiate class " + getConfiguration().getParserEngine());
			e.printStackTrace();
			System.exit(-2);
		} catch (IllegalAccessException e) {
			log.fatal("Failed to instantiate class " + getConfiguration().getParserEngine());
			e.printStackTrace();
			System.exit(-2);
		}
		if(parser instanceof CrosswordParser)
			prs = (CrosswordParser)parser;
		else {
			log.fatal("Class " + getConfiguration().getParserEngine() + " does not implement ParsingEngine interface");
			System.err.println("Unable to instantiate dictionary source. See logs.");
			System.exit(-2);
		}
		
		return prs;
	}
	
	// sets the solver engine dynamically instantiating the class read from the configuration file
	private SolvingEngine newSolvingEngine(){
		Class<?> solverClass = null;
		SolvingEngine slvr = null;
		try {
			solverClass = Class.forName(getConfiguration().getSolverEngine());
		} catch (ClassNotFoundException e) {
			log.fatal("Invalid SolvingEngine class: " + getConfiguration().getSolverEngine());
			e.printStackTrace();
			System.exit(-2);
		}
		Object solver = null;

		try {
			solver = solverClass.getConstructor(DictionarySource.class).newInstance(getDictionarySource());
		} catch (Exception e) {
			e.printStackTrace();
			log.fatal("Cannot instantiate class : " + getConfiguration().getSolverEngine() +": " +e.getMessage());
			System.exit(-2);
		}

	
		if(solver instanceof SolvingEngine)
			slvr = (SolvingEngine)solver;
		else {
			log.fatal("Class " + getConfiguration().getSolverEngine() + " does not implement SolvingEngine interface");
			System.err.println("Unable to instantiate Solver engine. See logs.");
			System.exit(-2);
		}
		
		return slvr;
	}
	
	




	
}
